/*
 * Copyright 2011 Christoph Ludwig (christoph[at]chludwig.de). All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.chludwig.ehumanities.provenance.resources;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.File;
import java.io.FileWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.ws.rs.core.MediaType;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.junit.Test;
import org.xml.sax.SAXException;

import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.WebAppDescriptor;
import com.sun.jersey.test.framework.WebAppDescriptor.Builder;
import com.sun.jersey.test.framework.spi.container.TestContainerException;

import de.chludwig.ehumanities.provenance.jaxb.EntryType;
import de.chludwig.ehumanities.provenance.jaxb.LogEntryMetadataType;
import de.chludwig.ehumanities.provenance.jaxb.LogEntryType;
import de.chludwig.ehumanities.provenance.jaxb.ObjectFactory;
import de.chludwig.ehumanities.provenance.jaxb.WorkflowContainerInitializerType;
import de.chludwig.ehumanities.provenance.jaxb.WorkflowContainerMetadataType;
import de.chludwig.ehumanities.provenance.jaxb.WorkflowContainerType;
import de.chludwig.ehumanities.provenance.jaxb.WorkflowContainerType.Entries;
import de.chludwig.ehumanities.provenance.jaxb.ZipInfoType;
import de.chludwig.ehumanities.provenance.servlet.HPTContextListener;
import de.chludwig.ehumanities.provenance.utils.ProvenanceContextUtil;

/**
 * HPT Servlet Integration Test
 */
public class ServletIT extends JerseyTest {

  private File dataFolder;

  /**
   * Application descriptor that declares the Provenance application.
   * 
   * @return a descriptor with javax.ws.rs.Application set to
   *         {@link ProvenanceApplication}.
   */
  private static WebAppDescriptor provenanceAppDescriptor(File dataDir) {
    try {
      String dataDirPath = dataDir.getAbsolutePath();
      Map<String, String> initParams = new HashMap<String, String>();
      initParams.put("javax.ws.rs.Application", "de.chludwig.ehumanities.provenance.resources.ProvenanceApplication");
      initParams.put("com.sun.jersey.spi.container.ContainerRequestFilters",
              "com.sun.jersey.api.container.filter.LoggingFilter");
      initParams.put("com.sun.jersey.spi.container.ContainerResponseFilters",
              "com.sun.jersey.api.container.filter.LoggingFilter");
      Builder builder = new WebAppDescriptor.Builder(initParams);
      builder = builder.contextParam(ProvenanceContextUtil.DE_CHLUDWIG_EHUMANITIES_PROVENANCE_DATADIRECTORY,
              dataDirPath);
      builder = builder.contextListenerClass(HPTContextListener.class);
      return builder.build();
    }
    catch (Exception ex) {
      throw new TestContainerException("cannot build web application descriptor", ex);
    }
  }

  private static File createTmpFixtureFolder() throws TestContainerException {
    Properties props = System.getProperties();
    props.list(System.err);
    File tmpDir = new File(System.getProperty("java.io.tmpdir"));
    MessageFormat filenameFormatter = new MessageFormat("provenance-{0}.unittest"); //$NON-NLS-1$
    for (int attempt = 0; attempt < 3; ++attempt) {
      String randomFilename = filenameFormatter.format(new Object[] { UUID.randomUUID() });
      File fixtureFolder = new File(tmpDir, randomFilename);
      if (fixtureFolder.mkdir()) {
        return fixtureFolder;
      }
    }
    String msgPattern = "cannot creature temporary folder in {0} for unittest fixture"; //$NON-NLS-1$
    String msg = MessageFormat.format(msgPattern, tmpDir.getAbsolutePath());
    throw new TestContainerException(msg);
  }

  /**
   * Default constructor.
   * 
   * @throws TestContainerException
   */
  public ServletIT() throws TestContainerException {
    this(createTmpFixtureFolder());
  }

  private ServletIT(File dataDir) throws TestContainerException {
    super(provenanceAppDescriptor(dataDir));
    setDataFolder(dataDir);
  }

  /**
   * @return the dataFolder
   */
  public File getDataFolder() {
    return dataFolder;
  }

  /**
   * @param dataFolder
   *          the dataFolder to set
   */
  private void setDataFolder(File dataFolder) {
    this.dataFolder = dataFolder;
  }

  // @Test
  public void testJAXB() throws Exception {
    WorkflowContainerInitializerType containerInit = new WorkflowContainerInitializerType();
    containerInit.setCreator("John Smith");
    String result = marshallWorkflowContainerInitializer(containerInit);
    assertNotNull(result);
    System.out.println(result);
  }

  @Test
  public void testUnmarshal() throws Exception {
    StringBuilder xmlBuilder = new StringBuilder();
    xmlBuilder.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
    xmlBuilder
            .append("<workflowContainerInitializer xmlns=\"http://www.chludwig.de/schemas/textgrid/provenance-tool/\">\n");
    xmlBuilder.append("  <creator>John Smith</creator>\n");
    xmlBuilder.append("</workflowContainerInitializer>");
    String xml = xmlBuilder.toString();
    JAXBContext jc = JAXBContext.newInstance("de.chludwig.ehumanities.provenance.jaxb");
    Unmarshaller u = jc.createUnmarshaller();
    u.setSchema(getWorkflowContainerSchema());
    JAXBElement<WorkflowContainerInitializerType> o = u.unmarshal(new StreamSource(new StringReader(xml)),
            WorkflowContainerInitializerType.class);
    assertNotNull(o);
    WorkflowContainerInitializerType containerInitializer = o.getValue();
    assertNotNull(containerInitializer);
    assertEquals("John Smith", containerInitializer.getCreator());
  }

  /**
   * @param containerInit
   * @return
   * @throws JAXBException
   * @throws PropertyException
   * @throws SAXException
   */
  protected String marshallWorkflowContainerInitializer(WorkflowContainerInitializerType containerInit)
          throws JAXBException, PropertyException, SAXException {
    JAXBElement<WorkflowContainerInitializerType> root = new ObjectFactory()
            .createWorkflowContainerInitializer(containerInit);
    Writer writer = new StringWriter();

    JAXBContext jc = JAXBContext.newInstance("de.chludwig.ehumanities.provenance.jaxb");
    Marshaller m = jc.createMarshaller();
    m.setSchema(getWorkflowContainerSchema());
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    m.marshal(root, writer);
    return writer.toString();
  }

  protected static Schema getWorkflowContainerSchema() throws SAXException {
    URL schemaURL = ProvenanceRoot.class.getClassLoader().getResource("schemas/workflowContainer.xsd");
    Schema schema;
    SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    schema = sf.newSchema(schemaURL);
    return schema;
  }

  @Test
  public void testWorkflowContainerCreation() throws Exception {
    String creator = "John Smith";

    WebResource workflowContainerResource = createWorkflowContainerResource(creator);
    UUID uuid = extractIdFromWorkflowContainerLocator(workflowContainerResource.getURI());
    assertNotNull(uuid);

    WorkflowContainerType workflowContainer = getWorkflowContainer(workflowContainerResource);
    checkWorkflowContainer(workflowContainer, creator, uuid, Collections.<UUID> emptySet());
  }

  @Test
  public void testMissingContainerAccess() throws Exception {
    WebResource fakeContainerResource = resource().path("container").path(UUID.randomUUID().toString());
    ClientResponse response = fakeContainerResource.get(ClientResponse.class);
    assertNotNull(response);
    Status responseCode = response.getClientResponseStatus();
    assertEquals(Status.NOT_FOUND, responseCode);
  }

  @Test
  public void testEntryCreation() throws Exception {
    String creator = "John Doe";

    WebResource workflowContainerResource = createWorkflowContainerResource(creator);
    UUID workflowUuid = extractIdFromWorkflowContainerLocator(workflowContainerResource.getURI());

    WebResource entry1Resource = createEntryResource(workflowContainerResource);
    UUID entry1Uuid = extractIdFromEntryLocator(workflowContainerResource, entry1Resource.getURI());

    LogEntryType logEntry1 = getLogEntry(entry1Resource);
    checkLogEntry(logEntry1, entry1Uuid);

    WebResource entry2Resource = createEntryResource(workflowContainerResource);
    UUID entry2Uuid = extractIdFromEntryLocator(workflowContainerResource, entry2Resource.getURI());

    LogEntryType logEntry2 = getLogEntry(entry2Resource);
    checkLogEntry(logEntry2, entry2Uuid);

    WorkflowContainerType workflowContainer = getWorkflowContainer(workflowContainerResource);

    Set<UUID> expectedEntries = new HashSet<UUID>();
    expectedEntries.add(entry1Uuid);
    expectedEntries.add(entry2Uuid);
    checkWorkflowContainer(workflowContainer, creator, workflowUuid, expectedEntries);

  }

  @Test
  public void testEntry() throws Exception {
    String creator = "John Doe";

    WebResource workflowContainerResource = createWorkflowContainerResource(creator);

    WebResource entry1Resource = createEntryResource(workflowContainerResource);
    UUID entry1Uuid = extractIdFromEntryLocator(workflowContainerResource, entry1Resource.getURI());

    LogEntryType logEntry1 = getLogEntry(entry1Resource);
    checkLogEntry(logEntry1, entry1Uuid);

    ClientResponse dataResponse = getData(logEntry1);

    LogEntryMetadataType entryMetadata = logEntry1.getMetadata();
    MediaType expectedMediaType = entryMetadata.getMediaType();
    String expectedContent = "";
    checkData(dataResponse, expectedMediaType, expectedContent);

    String content2 = "Hello World!";
    putData(logEntry1, MediaType.TEXT_PLAIN_TYPE, content2);

    LogEntryType logEntry1_2 = getLogEntry(entry1Resource);
    checkLogEntry(logEntry1_2, entry1Uuid);

    ClientResponse dataResponse2 = getData(logEntry1_2);

    LogEntryMetadataType entryMetadata2 = logEntry1_2.getMetadata();
    MediaType expectedMediaType2 = entryMetadata2.getMediaType();
    assertEquals(MediaType.TEXT_PLAIN_TYPE, expectedMediaType2);
    checkData(dataResponse2, expectedMediaType2, content2);
  }

  @Test
  public void testStartZipping() throws Exception {
    String creator = "John Zip";

    WebResource workflowContainerResource = createWorkflowContainerResource(creator);
    UUID workflowId = extractIdFromWorkflowContainerLocator(workflowContainerResource.getURI());

    WebResource entry1Resource = createEntryResource(workflowContainerResource);
    LogEntryType logEntry1 = getLogEntry(entry1Resource);

    String content2 = "Hello World!";
    putData(logEntry1, MediaType.TEXT_PLAIN_TYPE, content2);

    ClientResponse zipResponse = workflowContainerResource.path(WorkflowContainerResource.ZIP_PATH).post(
            ClientResponse.class);
    assertNotNull(zipResponse);

    Status zipStatus = zipResponse.getClientResponseStatus();
    assertNotNull(zipStatus);
    assertEquals(Status.ACCEPTED, zipStatus);

    URI zippingLocator = zipResponse.getLocation();
    assertNotNull(zippingLocator);

    WebResource zippingResource = resource().uri(zippingLocator);
    ClientResponse zippingResponse = zippingResource.get(ClientResponse.class);
    assertNotNull(zippingResponse);
    Status zippingStatus = zippingResponse.getClientResponseStatus();
    assertEquals(Status.OK, zippingStatus);

    ClientResponse goneResponse = workflowContainerResource.get(ClientResponse.class);
    assertNotNull(goneResponse);
    Status goneStatus = goneResponse.getClientResponseStatus();
    assertEquals(Status.GONE, goneStatus);

    File zipFolder = new File(getDataFolder(), "zip");
    zipFolder.mkdir();
    File fakeZip = new File(zipFolder, workflowId.toString() + ".zip");
    Writer writer = new FileWriter(fakeZip);
    writer.write("This is a fake zip file.\n");
    writer.close();

    ClientResponse zippingResponse2 = zippingResource.get(ClientResponse.class);
    assertNotNull(zippingResponse2);
    Status zippingStatus2 = zippingResponse2.getClientResponseStatus();
    assertEquals(Status.OK, zippingStatus2);

  }

  @Test
  public void testZipArchive() throws Exception {
    String creator = "John Zip";

    WebResource workflowContainerResource = createWorkflowContainerResource(creator);

    createEntryResource(workflowContainerResource);

    ClientResponse zipResponse = workflowContainerResource.path(WorkflowContainerResource.ZIP_PATH).post(
            ClientResponse.class);
    assertNotNull(zipResponse);
    Status zipStatus = zipResponse.getClientResponseStatus();
    assertNotNull(zipStatus);
    assertEquals(Status.ACCEPTED, zipStatus);

    URI zippingLocator = zipResponse.getLocation();
    assertNotNull(zippingLocator);

    WebResource zippingResource = resource().uri(zippingLocator);
    URI zipLocator = null;
    for (int i = 0; i < 3 && zipLocator == null; ++i) {
      ClientResponse zippingResponse = zippingResource.get(ClientResponse.class);
      assertNotNull(zippingResponse);
      Status zippingStatus = zippingResponse.getClientResponseStatus();
      assertEquals(Status.OK, zippingStatus);
      ZipInfoType zipInfo = zippingResponse.getEntity(ZipInfoType.class);

      String zipArchiveLocator = zipInfo.getZipArchiveLocator();
      if(zipArchiveLocator != null) {
        zipLocator = URI.create(zipArchiveLocator);
      }
      else {
        TimeUnit.SECONDS.sleep(5);
      }
    }
    assertNotNull(zipLocator);
    
    WebResource zipResource = resource().uri(zipLocator);
    ClientResponse zipResponse2 = zipResource.get(ClientResponse.class);
    ZipInputStream zipInputStream = new ZipInputStream(zipResponse2.getEntityInputStream());
    for(ZipEntry entry = zipInputStream.getNextEntry(); entry != null; entry = zipInputStream.getNextEntry()) {
      System.out.println(entry.getName());
    }
  }

  @Test
  public void testWadl() throws Exception {
    WebResource wadlResource = resource().path("application.wadl");
    String wadl = wadlResource.get(String.class);
    assertNotNull(wadl);
  }

  /**
   * @param workflowContainerResource
   * @return
   */
  private WebResource createEntryResource(WebResource workflowContainerResource) {
    ClientResponse createEntryResponse = workflowContainerResource.path(WorkflowContainerResource.ENTRY_PATH_PREFIX)
            .post(ClientResponse.class);
    assertNotNull(createEntryResponse);

    // check for the 201 return code and the location header
    Status createEntryStatus = createEntryResponse.getClientResponseStatus();
    assertEquals(Status.CREATED, createEntryStatus);

    URI entryLocation1 = createEntryResponse.getLocation();
    assertNotNull(entryLocation1);
    URI entryLocation = entryLocation1;
    WebResource entryResource = resourceFromLocation(entryLocation);
    return entryResource;
  }

  /**
   * @param creator
   * @return
   * @throws JAXBException
   * @throws PropertyException
   * @throws SAXException
   */
  private WebResource createWorkflowContainerResource(String creator) throws JAXBException, PropertyException,
          SAXException {
    WorkflowContainerInitializerType containerInit = new WorkflowContainerInitializerType();
    containerInit.setCreator(creator);
    WebResource applicationRoot = resource();
    ClientResponse response = applicationRoot.entity(marshallWorkflowContainerInitializer(containerInit),
            MediaType.APPLICATION_XML_TYPE).post(ClientResponse.class);
    assertNotNull(response);

    // check for the 201 return code and the location header
    // (whitebox testing: we know the URL scheme and can thus extract the
    // generated ID)
    Status clientResponseStatus = response.getClientResponseStatus();
    assertEquals(Status.CREATED, clientResponseStatus);
    URI location1 = response.getLocation();
    assertNotNull(location1);
    URI location = location1;
    WebResource workflowContainerResource = resourceFromLocation(location);
    return workflowContainerResource;
  }

  /**
   * @param dataResponse
   * @param expectedMediaType
   * @param expectedContent
   */
  private void checkData(ClientResponse dataResponse, MediaType expectedMediaType, String expectedContent) {
    MediaType dataMediaType = dataResponse.getType();
    assertNotNull(dataMediaType);
    assertEquals(expectedMediaType, dataMediaType);
    String dataContent = dataResponse.getEntity(String.class);
    assertEquals(expectedContent, dataContent);
  }

  /**
   * @param logEntry
   * @return
   * @throws URISyntaxException
   */
  private ClientResponse getData(LogEntryType logEntry) throws URISyntaxException {
    URI dataUri = new URI(logEntry.getDataLocator());
    WebResource dataResource = resourceFromLocation(dataUri);
    ClientResponse dataResponse = dataResource.get(ClientResponse.class);
    assertNotNull(dataResponse);
    Status dataStatus = dataResponse.getClientResponseStatus();
    assertNotNull(dataStatus);
    assertEquals(Status.OK, dataStatus);
    return dataResponse;
  }

  private ClientResponse putData(LogEntryType logEntry, MediaType mediaType, String content) throws URISyntaxException {
    URI dataUri = new URI(logEntry.getDataLocator());
    WebResource dataResource = resourceFromLocation(dataUri);
    ClientResponse dataResponse = dataResource.entity(content, mediaType).put(ClientResponse.class);
    assertNotNull(dataResponse);
    Status dataStatus = dataResponse.getClientResponseStatus();
    assertNotNull(dataStatus);
    assertEquals(Status.NO_CONTENT, dataStatus);
    return dataResponse;
  }

  /**
   * @param logEntry
   * @param expectedEntryUuid
   */
  private void checkLogEntry(LogEntryType logEntry, UUID expectedEntryUuid) {
    LogEntryMetadataType entryMetadata = logEntry.getMetadata();
    assertEquals(entryMetadata.getUuid(), expectedEntryUuid);
  }

  /**
   * @param entryResource
   * @return
   */
  private LogEntryType getLogEntry(WebResource entryResource) {
    ClientResponse entryResourceResponse = entryResource.get(ClientResponse.class);
    assertNotNull(entryResourceResponse);
    Status entryResourceStatus = entryResourceResponse.getClientResponseStatus();
    assertEquals(Status.OK, entryResourceStatus);

    LogEntryType logEntry = entryResourceResponse.getEntity(LogEntryType.class);
    assertNotNull(logEntry);
    return logEntry;
  }

  /**
   * @param workflowContainerResource
   * @param entryLocation
   * @return
   */
  private UUID extractIdFromEntryLocator(WebResource workflowContainerResource, URI entryLocation) {
    String entryId = workflowContainerResource.path("entry").getURI().relativize(entryLocation).toString();
    UUID entryUuid = UUID.fromString(entryId);
    assertNotNull(entryUuid);
    return entryUuid;
  }

  /**
   * @param workflowContainer
   * @param expectedCreator
   * @param expectedUuid
   * @param list
   */
  private void checkWorkflowContainer(WorkflowContainerType workflowContainer, String expectedCreator,
          UUID expectedUuid, Set<UUID> expectedEntries) {
    WorkflowContainerMetadataType workflowMetadata = workflowContainer.getMetadata();
    assertNotNull(workflowMetadata);
    UUID workflowUuid = workflowMetadata.getUuid();
    assertNotNull(workflowUuid);
    assertEquals(expectedUuid, workflowUuid);
    String workflowCreator = workflowMetadata.getCreator();
    assertNotNull(workflowCreator);
    assertEquals(expectedCreator, workflowCreator);
    Entries entries = workflowContainer.getEntries();
    assertNotNull(entries);
    Set<UUID> entriesSet = new HashSet<UUID>();
    for (EntryType entry : entries.getEntry()) {
      entriesSet.add(entry.getEntry().getMetadata().getUuid());
    }
    assertEquals(expectedEntries, entriesSet);
  }

  /**
   * @param location
   * @return
   */
  private WebResource resourceFromLocation(URI location) {
    return resource().uri(location);
  }

  /**
   * @param workflowContainerResource
   * @return
   */
  private WorkflowContainerType getWorkflowContainer(WebResource workflowContainerResource) {
    ClientResponse workflowContainerResponse = workflowContainerResource.get(ClientResponse.class);
    assertNotNull(workflowContainerResponse);
    Status workflowContainerStatus = workflowContainerResponse.getClientResponseStatus();
    assertEquals(Status.OK, workflowContainerStatus);

    // check that its ID and creator equal the values above and that there is no
    // entry yet.
    WorkflowContainerType workflowContainer = workflowContainerResponse.getEntity(WorkflowContainerType.class);
    assertNotNull(workflowContainer);
    return workflowContainer;
  }

  /**
   * @param location
   * @return
   */
  private UUID extractIdFromWorkflowContainerLocator(URI location) {
    URI reference = resource().path("container").getURI();
    String id = reference.relativize(location).toString();
    UUID uuid = UUID.fromString(id);
    return uuid;
  }

}
